package com.neuronbit.businessflow.plugin;

import com.neuronbit.businessflow.flow.project.AbstractProject;
import com.neuronbit.businessflow.flow.util.JarFileUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FileUtils;
import org.gradle.api.Project;
import org.gradle.api.artifacts.component.ComponentIdentifier;
import org.gradle.api.artifacts.component.LibraryBinaryIdentifier;
import org.gradle.api.artifacts.component.ProjectComponentIdentifier;
import org.gradle.api.artifacts.result.*;
import org.gradle.api.plugins.JavaPluginExtension;
import org.gradle.jvm.JvmLibrary;
import org.gradle.language.base.artifact.SourcesArtifact;

import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.util.*;
import java.util.stream.Collectors;

/**
 * The type Gradle project.
 */
@Slf4j
public class GradleProject extends AbstractProject {
    private final Project project;
    private final Set<Project> allProjects;
    private final Set<? extends DependencyResult> runtimeClasspath;

    /**
     * Instantiates a new Gradle project.
     *
     * @param project  the project
     * @param packages the packages
     * @throws IOException any io related exception
     */
    private GradleProject(Project project, String packages) throws IOException {
        this(project, packages, "");
    }

    /**
     * Instantiates a new Gradle project.
     *
     * @param project  the project
     * @param packages packages which contains API definitions
     * @param include  scan the packages in the following artifacts, separate multiple artifact name by comma.
     * @throws IOException any io related exception
     */
    private GradleProject(Project project, String packages, String include) throws IOException {
        super(packages, include);
        this.project = project;
        this.allProjects = project.getRootProject().getAllprojects();
        runtimeClasspath = project.getConfigurations().getByName("runtimeClasspath")
                .getIncoming()
                .getResolutionResult()
                .getAllDependencies();
    }

    @Override
    protected String getProjectName() {
        return project.getName();
    }

    @Override
    protected Path getProjectPath() {
        return project.getProjectDir().toPath();
    }

    @Override
    protected Set<File> resolveSrcDirs() {
        Set<File> srcDirs = new HashSet<>();
        //current project
        srcDirs.addAll(project.getExtensions().getByType(JavaPluginExtension.class)
                .getSourceSets()
                .getByName("main")
                .getJava()
                .getSrcDirs()
                .stream()
                .filter(d -> d.exists() && d.isDirectory())
                .collect(Collectors.toSet()));
        //dependent modules in the same root project
        srcDirs.addAll(
                runtimeClasspath
                        .stream()
                        .filter(dr -> dr instanceof ResolvedDependencyResult)
                        .map(dr -> ((ResolvedDependencyResult) dr).getSelected().getId())
                        .filter(id -> id instanceof ProjectComponentIdentifier)
                        .map(id -> {
                            final ProjectComponentIdentifier componentIdentifier = (ProjectComponentIdentifier) id;
                            log.info("try to find project component {}", componentIdentifier.getProjectName());
                            return allProjects.stream().filter(p -> p.getName().equals(componentIdentifier.getProjectName())).findFirst();
                        })
                        .filter(Optional::isPresent)
                        .map(p -> p.get().getExtensions().getByType(JavaPluginExtension.class).getSourceSets().getByName("main").getJava().getSrcDirs())
                        .flatMap(Collection::stream)
                        .filter(d -> d.exists() && d.isDirectory())
                        .collect(Collectors.toSet())
        );

        return srcDirs;
    }

    @Override
    protected Set<File> resolveIncludeDirs() throws IOException {
        final String[] additionalArtifacts = getInclude();
        if (additionalArtifacts == null || additionalArtifacts.length == 0) {
            return Collections.emptySet();
        }
        Set<File> additionalDirs = new HashSet<>();
        //dependent modules in the same root project
        additionalDirs.addAll(runtimeClasspath
                .stream()
                .filter(dr -> dr instanceof ResolvedDependencyResult)
                .map(dr -> ((ResolvedDependencyResult) dr).getSelected().getId())
                .map(id -> {
                    if (id instanceof ProjectComponentIdentifier) {
                        final ProjectComponentIdentifier componentIdentifier = (ProjectComponentIdentifier) id;
                        log.info("try to find project component {}", componentIdentifier.getProjectName());
                        for (String artifact : additionalArtifacts) {
                            if (artifact.equals(componentIdentifier.getProjectName())) {
                                return allProjects.stream().filter(p -> p.getName().equals(artifact)).findFirst();
                            }
                        }
                    }

                    return Optional.<Project>empty();
                })
                .filter(Optional::isPresent)
                .map(p -> p.get().getExtensions().getByType(JavaPluginExtension.class).getSourceSets().getByName("main").getJava().getSrcDirs())
                .flatMap(Collection::stream)
                .filter(d -> d.exists() && d.isDirectory())
                .collect(Collectors.toSet()));
        //source jar
        final List<ComponentIdentifier> collected = runtimeClasspath
                .stream()
                .filter(dr -> dr instanceof ResolvedDependencyResult)
                .map(dr -> ((ResolvedDependencyResult) dr).getSelected().getId())
                .filter(id -> {
                    if (id instanceof LibraryBinaryIdentifier) {
                        for (String artifact : additionalArtifacts) {
                            //TODO spearwang 2024/7/5: filter group id
                            if (artifact.equals(((LibraryBinaryIdentifier) id).getLibraryName())) {
                                return true;
                            }
                        }
                        return false;
                    } else {
                        return false;
                    }
                })
                .collect(Collectors.toList());

        if (collected.isEmpty()) {
            return additionalDirs;
        }

        final ArtifactResolutionResult result = project
                .getDependencies()
                .createArtifactResolutionQuery()
                .forComponents(collected)
                .withArtifacts(JvmLibrary.class, SourcesArtifact.class)
                .execute();
        final Set<String> fileNames = result.getResolvedComponents().stream()
                .map(comp -> {
                    final Set<ArtifactResult> artifacts = comp.getArtifacts(SourcesArtifact.class);
                    for (ArtifactResult artifact : artifacts) {
                        if (artifact instanceof ResolvedArtifactResult) {
                            return ((ResolvedArtifactResult) artifact).getFile().getName();
                        }
                    }
                    return null;
                })
                .filter(Objects::nonNull)
                .collect(Collectors.toSet());
        if (fileNames.isEmpty()) {
            return additionalDirs;
        }

        File[] sourceJarFiles = get3rdPartyFolder().listFiles(fn -> fn.getName().endsWith("-sources.jar") && fn.isFile());
        if (Objects.nonNull(sourceJarFiles)) {
            for (File sourceJarFile : sourceJarFiles) {
                if (fileNames.contains(sourceJarFile.getName())) {
                    final File unzipedDir = get3rdPartyFolder().toPath().resolve(sourceJarFile.getName()).toFile();
                    JarFileUtil.unzipJarFile(sourceJarFile, unzipedDir);
                    additionalDirs.add(unzipedDir);
                    FileUtils.deleteQuietly(sourceJarFile);
                }
            }
        }

        return additionalDirs;
    }

    @Override
    public Path getOutputPath() {
        return project.getLayout().getBuildDirectory().get().getAsFile().toPath().resolve("generated/main/java");
    }

    /**
     * The type Builder.
     */
    public static class Builder {
        private final GradleProject gradleProject;

        /**
         * Instantiates a new Gradle project.
         *
         * @param project  the project
         * @param packages packages which contains API definitions
         * @param include  scan the packages in the following artifacts, separate multiple artifact name by comma.
         * @throws IOException any io related exception
         */
        public Builder(Project project, String packages, String include) throws IOException {
            gradleProject = new GradleProject(project, packages, include);
        }

        /**
         * Instantiates a new Gradle project.
         *
         * @param project  the project
         * @param packages the packages
         * @throws IOException any io related exception
         */
        public Builder(Project project, String packages) throws IOException {
            gradleProject = new GradleProject(project, packages);
        }


        /**
         * Build com . webank . apihub . model . project . project.
         *
         * @return the com . webank . apihub . model . project . project
         * @throws IOException the io exception
         */
        public com.neuronbit.businessflow.flow.project.Project build() throws IOException {
            return gradleProject.build();
        }
    }
}
